医疗大模型
[unisound 实习] CHIP 智能病历质控
[unisound 实习] CHIP 智能病历质控项目说明
1. 项目概述
本项目来源于 CHIP 智能病历质控赛题。赛题给定 19 个病历质控点,要求系统检查入院病历是否存在信息缺失、书写不规范、内容矛盾或诊断质量问题。
项目采用三阶段流程:
原始入院病历
↓
阶段一:病历理解与结构化
↓
阶段二:19项病历质控
↓
阶段三:Thinking模型复核
↓
病历级质控结果
↓
按record_id和rule_id展开
↓
SFT / DAPO训练数据
CHIP项目的核心产物不是一个直接训练的模型,而是一批经过结构化、质控和复核的病历质控数据。这些数据可以转换为SFT或DAPO格式,提供给下游 llm_cft-ms_swift 项目进行模型微调。
需要明确:19个质控点及其定义来自赛题,本项目负责实现这些任务并完成数据生产,不是自行设计了19项医疗标准。
2. 原始数据
2.1 原始数据格式
原始文件是一个JSON数组,一条记录对应一份入院病历:
{
"record_id": "MED_QC_ADM_0001",
"record_type": "ADM_NOTE",
"emr_content": "主诉、现病史、既往史、检查和诊断等完整病历文本"
}
字段含义:
record_id:病例唯一标识;record_type:病例类型,本项目主要是入院记录ADM_NOTE;emr_content:一整段半结构化病历文本。
2.2 emr_content包含的内容
emr_content通常包含:
- 主诉;
- 现病史;
- 既往史;
- 个人史;
- 家族史;
- 月经史、婚育史;
- 体格检查;
- 专科检查;
- 辅助检查;
- 入院诊断;
- 初步诊断;
- 入院主诊断、修正主诊断等可选内容。
数据中的姓名、时间、温度和血压等敏感信息可能使用 * 脱敏,缺失章节可能写成 None、空字符串或完全不出现。
3. 阶段一:病历理解与结构化
3.1 整体结构化
第一阶段调用32B医疗大模型,将 emr_content 拆分为字段明确的结构化JSON。至少要求以下7个字段存在:
主诉、现病史、既往史、专科检查、辅助检查、入院诊断、初步诊断
字段必须存在,但内容可以为空。例如原文没有专科检查时,应输出空字符串;没有诊断时,应输出空列表。
转换前:
{
"record_id": "MED_QC_ADM_0001",
"emr_content": "主诉:反复发热两周\n现病史:患者三天前出现发热……"
}
转换后:
{
"record_id": "MED_QC_ADM_0001",
"emr_content": "原始完整病历文本",
"json": {
"主诉": "反复发热两周",
"现病史": "患者三天前出现发热……",
"既往史": "否认高血压、糖尿病史……",
"专科检查": "",
"辅助检查": "胸部CT提示肺部结节……",
"入院诊断": ["发热", "肾功能不全"],
"初步诊断": ["发热", "血管炎待排"]
}
}
原始 emr_content 继续保留,用于信息追溯和后续复核。
3.2 诊断字段统一
模型有时会把入院诊断或初步诊断输出为字符串。程序通过本地 ensure_list() 函数将它们统一为列表:
"发热" → ["发热"]
这一步使用本地程序,不调用LLM。
3.3 手术和操作实体抽取
结构化完成后,再调用两次LLM:
- 从现病史中抽取手术或医疗操作实体;
- 从既往史中抽取手术或医疗操作实体。
例如:
现病史:患者一月前行软性输尿管镜钬激光碎石术,术后留置输尿管内支架。
抽取结果:
[
"软性输尿管镜钬激光碎石术",
"留置输尿管内支架"
]
程序随后调用ICD编码服务,并对照《手术操作分类代码国家临床版2.0》判断实体是否属于手术:
{
"现病史实体": [
"软性输尿管镜钬激光碎石术 属于手术",
"留置输尿管内支架 不属于手术"
],
"既往史实体": []
}
这些实体主要用于判断现病史与既往史中的手术史是否矛盾。
3.4 结构化幻觉控制
大模型可能增加原文不存在的信息、改变否定关系或遗漏字段。合理的质量控制方式是,在模型输出JSON后执行硬编码检查:
- 检查JSON是否可以解析;
- 检查7个必要字段是否存在;
- 检查字段类型是否符合约定;
- 归一化空格、换行和标点后,检查字段内容能否回溯到原始
emr_content; - 无法回溯的内容标记为疑似幻觉,重新抽取;重复失败则丢弃。
需要注意:原始仓库完成了字段存在性和诊断列表转换等处理,但“逐字段原文回溯并自动丢弃”的严格流程属于后续完善方案,不能把它描述成当前代码已经完整实现的功能。
4. 阶段二:19项病历质控
4.1 质控任务分类
19个质控点可以分为四类:
| 类型 | 数量 | 主要内容 |
|---|---|---|
| 信息完整性 | 7项 | 主诉、现病史、既往史和诊断信息是否缺失 |
| 病历规范性 | 6项 | 症状、检查、用药及诊断记录是否规范 |
| 内容一致性 | 4项 | 主诉、现病史和既往史是否前后矛盾 |
| 诊断质量 | 2项 | 诊断是否合理、是否仅使用症状名称 |
| 合计 | 19项 | — |
19项规则如下:
| rule_id | 检查内容 |
|---|---|
IC-ZS-01-V1 |
主诉是否包含病情信息和持续时间 |
EN-ZS-01-V1 |
主诉症状数量、长度和表达是否规范 |
IC-XB-01-V1 |
现病史是否记录入院前治疗经过 |
IC-XB-02-V1 |
是否记录与主诊断相关的阳性、阴性症状 |
IC-XB-03-V1 |
是否记录饮食、睡眠、大小便等一般情况 |
CO-XB-01-V1 |
主诉与现病史中的临床表现是否矛盾 |
CO-XB-02-V1 |
手术史等病史信息前后是否一致 |
CO-XB-03-V1 |
主诉与现病史是否相关、相符 |
CO-XB-04-V1 |
主诉与现病史中的时间是否一致 |
EN-XB-01-V1 |
主要症状特点和发展变化是否记录规范 |
EN-XB-02-V1 |
发病时间、诱因等是否记录规范 |
IC-JW-01-V1 |
慢性病、手术史等是否记录完整 |
EN-JW-01-V1 |
用药、手术情况是否记录规范 |
IC-ZK-01-V1 |
专科检查是否遗漏阳性体征 |
EN-FZ-01-V1 |
辅助检查是否包含时间、地点、项目和结果 |
DQ-RZ-01-V1 |
病历信息能否支持主要诊断 |
DQ-RZ-02-V1 |
诊断是否仅为症状或体征名称 |
IC-RZCB-01-V1 |
诊断列表是否存在遗漏 |
EN-RZCB-01-V1 |
诊断顺序是否规范 |
4.2 硬编码与LLM的分工
硬编码处理能够确定判断的特殊情况,例如:
- 主诉、现病史、既往史或诊断为空;
- 主诉长度超过20;
- 专科检查或辅助检查为空时跳过对应规则;
- 模型输出JSON格式是否合法。
其余情况由医疗LLM结合独立Prompt判断。正常字段都不为空时,19项规则都可能调用LLM,因此硬编码主要承担前置快速判断,LLM承担核心语义质控。
4.3 Prompt与模型输出
每个质控点都有独立Prompt。程序从结构化JSON中取出相关字段,填入Prompt后调用医疗LLM。
例如 CO-XB-04-V1 检查主诉与现病史时间是否一致:
判断规则:主诉与现病史中的发病时间应保持一致。
主诉:反复发热两周
现病史:患者三天前出现发热……
请输出:
{"right":"合格/不合格","evidence":"判断依据"}
模型返回:
{
"right": "不合格",
"evidence": "主诉记录发热两周,现病史记录三天前发热,时间不一致"
}
right表示当前规则是否合格,evidence表示不合格依据。合格时Evidence应为空。
4.4 problems字段
模型返回不合格后,程序将结果转换并加入 problems:
{
"problems": [
{
"field": "history_present",
"issue_type": "CONSISTENCY",
"rule_id": "CO-XB-04-V1",
"description": "主诉记录发热两周,现病史记录三天前发热,时间不一致"
}
]
}
映射关系是:
right="不合格" → 加入problems
evidence → description
当前质控规则 → rule_id、field、issue_type
规则没有出现在 problems 中,表示该规则被判断为合格。
第二阶段同时保存每个规则实际使用的Prompt,形成:
{
"prompt": {
"CO-XB-04-V1": "该质控点实际使用的完整Prompt"
}
}
5. 阶段三:Thinking模型复核
第三阶段使用Thinking模型复核医疗LLM生成的质控问题。复核输入包括:
- 当前
rule_id和质控标准; - 相关病历字段;
- 初次质控问题及
description; - 常见症状、慢性病和通俗用语等外部知识。
Thinking模型判断:
质控结果正确:初次问题成立;质控结果不正确:初次问题可能是误报。
审核后,问题对象会增加:
{
"rule_id": "CO-XB-04-V1",
"description": "主诉与现病史时间不一致",
"whether_to_check": "是",
"check_res": "质控结果正确"
}
原始代码主要完成“复核和标记”,并没有真正从 problems 中删除 check_res=质控结果不正确 的问题。若要生产严格筛选后的训练数据,还需要增加过滤步骤。
Thinking模型适合复核复杂语义判断,但不能视为绝对金标,因为它仍然可能产生误判和遗漏。
6. CHIP最终病历级数据
经过三个阶段后,一份病例大致为:
{
"record_id": "MED_QC_ADM_0001",
"record_type": "ADM_NOTE",
"emr_content": "原始完整病历文本",
"json": {
"主诉": "反复发热两周",
"现病史": "患者三天前出现发热……",
"既往史": "否认高血压、糖尿病史……",
"专科检查": "",
"辅助检查": "胸部CT提示肺部结节……",
"入院诊断": ["发热", "肾功能不全"],
"初步诊断": ["发热", "血管炎待排"]
},
"现病史实体": [],
"既往史实体": [],
"problems": [
{
"field": "history_present",
"issue_type": "CONSISTENCY",
"rule_id": "CO-XB-04-V1",
"description": "主诉与现病史时间不一致",
"whether_to_check": "是",
"check_res": "质控结果正确"
}
],
"prompt": {
"CO-XB-04-V1": "该质控点实际使用的完整Prompt"
}
}
这仍然是病历级中间结果,还不是SFT或DAPO训练格式。
7. 从病历级结果到规则级训练数据
7.1 按规则展开
一份病历需要按19个 rule_id 展开:
一份病历 × 一个rule_id = 一条规则级训练样本
如果某规则出现在审核通过的 problems 中:
{
"right": "不合格",
"evidence": "对应problem中的description"
}
如果规则没有出现在 problems 中:
{
"right": "合格",
"evidence": ""
}
规则级通用数据为:
{
"record_id": "MED_QC_ADM_0001",
"rule_id": "CO-XB-04-V1",
"prompt": "质控规则、输出要求和相关病历内容",
"right": "不合格",
"evidence": "主诉与现病史时间不一致"
}
Prompt内部包含当前规则需要的病历内容,通常不再单独保存完整 emr_content。
7.2 SFT格式
SFT把标准答案放进assistant消息:
{
"messages": [
{
"role": "user",
"content": "质控规则、输出要求和相关病历内容"
},
{
"role": "assistant",
"content": "{\"right\":\"不合格\",\"evidence\":\"主诉与现病史时间不一致\"}"
}
],
"record_id": "MED_QC_ADM_0001",
"rule_id": "CO-XB-04-V1"
}
训练时模型直接模仿标准答案。
7.3 DAPO格式
DAPO只在 messages 中保留输入,把标准答案放在 solution 中:
{
"messages": [
{
"role": "user",
"content": "质控规则、输出要求和相关病历内容 /no_think"
}
],
"solution": "{\"right\":\"不合格\",\"evidence\":\"主诉与现病史时间不一致\"}",
"thinking": "0",
"record_id": "MED_QC_ADM_0001",
"rule_id": "CO-XB-04-V1"
}
DAPO训练时,模型先生成多个答案,奖励函数再将生成结果与 solution 比较:
right直接比较是否一致;evidence使用ROUGE-L计算文本重合程度;- 输出格式正确时获得额外格式奖励。
因此,同一批CHIP数据既可以转换成SFT,也可以转换成DAPO,区别主要是标准答案的位置和训练方式。
8. 实际数据结果
以下结果来自当前项目文件的实际统计,不是估算值。
8.1 原始病例与中间结果
| 数据文件 | 病例数 | 唯一record_id | problems数量 |
|---|---|---|---|
data/train.json |
300 | 300 | 924 |
data/train_更新_0929.json |
300 | 300 | 912 |
data/test_B_gold.json |
300 | 300 | 823 |
data/final_test_set.json |
300 | 300 | 823 |
data/to_test_6(数据构建结果).json |
300 | 300 | 924 |
to_test_6(数据构建结果).json虽然扩展名是 .json,实际内容是JSONL:每行保存一份结构化病例。
8.2 规则级训练数据
300份病例、19项任务理论上最多产生:
300 × 19 = 5,700条
实际下游数据为:
| 文件 | 样本数 | 病例数 | 规则数 |
|---|---|---|---|
to_train_compare.jsonl |
5,280 | 300 | 19 |
720_to_train_dapo.jsonl |
5,280 | 300 | 19 |
实际比理论最大值少420条,主要是部分规则在当前病例中不适用或被跳过。
8.3 标签分布
5,280条规则级数据中:
| 标签 | 数量 | 占比 |
|---|---|---|
| 合格 | 4,515 | 85.5% |
| 不合格 | 765 | 14.5% |
| 合计 | 5,280 | 100% |
数据存在明显类别不均衡,训练和评测时不能只看Accuracy,还需要关注不合格类的Precision、Recall、F1以及分规则指标。
8.4 数据集划分
SFT和DAPO使用同一批5,280条规则级样本,只是格式不同。数据按 record_id 划分:
| 集合 | 样本数 | 病例数 |
|---|---|---|
| Train | 4,224 | 240 |
| Validation | 521 | 30 |
| Test | 535 | 30 |
| 合计 | 5,280 | 300 |
同一份病例产生的所有规则级样本只进入一个集合,避免相同病历文本同时出现在训练集和测试集中造成数据泄漏。
9. 项目总结
完整数据链路可以概括为:
300份原始入院病历
↓ 32B医疗LLM
结构化病历JSON
↓ 两次实体抽取 + ICD服务
手术/操作实体
↓ 硬编码前置判断 + 19个分规则Prompt
病历质控problems
↓ Thinking模型
质控结果复核标记
↓ 按record_id、rule_id展开
5,280条规则级数据
├── SFT格式
└── DAPO格式
项目的主要价值包括:
- 将非结构化病历转换为统一字段;
- 将赛题规定的19项质控任务实现为可执行Prompt;
- 使用确定性规则处理明确问题,使用医疗LLM处理复杂语义判断;
- 使用Thinking模型复核初次质控结果;
- 将300份病例展开为5,280条规则级数据,并完成病历级隔离划分;
- 为下游SFT和DAPO微调提供统一数据来源。
当前代码仍存在配置字段缺失、合格结果可能向 problems 写入 None、复核后未实际删除误报以及绝对路径依赖等问题。如果需要将项目作为稳定的数据生产系统继续使用,应先修复这些工程问题,再重新运行并统计严格筛选后的最终数据。